<template>
  <div class="BgAnimation">
    <div class="waves" />
  </div>
</template>

<script>
class ShaderProgram {
  constructor(holder, options = {}) {
    options = Object.assign(
      {
        antialias: false,
        depthTest: false,
        mousemove: false,
        autosize: true,
        side: 'front',
        vertex: `
        precision highp float;
        attribute vec4 a_position;
        attribute vec4 a_color;
        uniform float u_time;
        uniform vec2 u_resolution;
        uniform vec2 u_mousemove;
        uniform mat4 u_projection;
        varying vec4 v_color;
        void main() {
          gl_Position = u_projection * a_position;
          gl_PointSize = (10.0 / gl_Position.w) * 100.0;
          v_color = a_color;
        }`,
        fragment: `
        precision highp float;
        uniform sampler2D u_texture;
        uniform int u_hasTexture;
        varying vec4 v_color;
        void main() {
          if ( u_hasTexture == 1 ) {
            gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);
          } else {
            gl_FragColor = v_color;
          }
        }`,
        uniforms: {},
        buffers: {},
        camera: {},
        texture: null,
        onUpdate: () => {},
        onResize: () => {}
      },
      options
    )
    const uniforms = Object.assign(
      {
        time: {
          type: 'float',
          value: 0
        },
        hasTexture: {
          type: 'int',
          value: 0
        },
        resolution: {
          type: 'vec2',
          value: [0, 0]
        },
        mousemove: {
          type: 'vec2',
          value: [0, 0]
        },
        projection: {
          type: 'mat4',
          value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
        }
      },
      options.uniforms
    )
    const buffers = Object.assign(
      {
        position: {
          size: 3,
          data: []
        },
        color: {
          size: 4,
          data: []
        }
      },
      options.buffers
    )
    const camera = Object.assign(
      {
        fov: 60,
        near: 1,
        far: 10000,
        aspect: 1,
        z: 100,
        perspective: true
      },
      options.camera
    )
    const canvas = document.createElement('canvas')
    const gl = canvas.getContext('webgl', {
      antialias: options.antialias
    })
    if (!gl) return false
    this.count = 0
    this.gl = gl
    this.canvas = canvas
    this.camera = camera
    this.holder = holder
    this.onUpdate = options.onUpdate
    this.onResize = options.onResize
    this.data = {}
    holder.appendChild(canvas)
    this.createProgram(options.vertex, options.fragment)
    this.createBuffers(buffers)
    this.createUniforms(uniforms)
    this.updateBuffers()
    this.updateUniforms()
    this.createTexture(options.texture)
    gl.enable(gl.BLEND)
    gl.enable(gl.CULL_FACE)
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE)
    gl[options.depthTest ? 'enable' : 'disable'](gl.DEPTH_TEST)
    if (options.autosize) { window.addEventListener('resize', e => this.resize(e), false) }
    if (options.mousemove) { window.addEventListener('mousemove', e => this.mousemove(e), false) }
    this.resize()
    this.update = this.update.bind(this)
    this.time = {
      start: performance.now(),
      old: performance.now()
    }
    this.update()
  }
  mousemove(e) {
    const x = (e.pageX / this.width) * 2 - 1
    const y = (e.pageY / this.height) * 2 - 1
    this.uniforms.mousemove = [x, y]
  }
  resize(e) {
    const holder = this.holder
    const canvas = this.canvas
    const gl = this.gl
    const width = (this.width = holder.offsetWidth)
    const height = (this.height = holder.offsetHeight)
    const aspect = (this.aspect = width / height)
    const dpi = (this.dpi = devicePixelRatio)
    canvas.width = width * dpi
    canvas.height = height * dpi
    canvas.style.width = 100 + '%'
    canvas.style.height = 100 + '%'
    gl.viewport(0, 0, width * dpi, height * dpi)
    gl.clearColor(0, 0, 0, 0)
    this.uniforms.resolution = [width, height]
    this.uniforms.projection = this.setProjection(aspect)
    this.onResize(width, height, dpi)
  }
  setProjection(aspect) {
    const camera = this.camera
    if (camera.perspective) {
      camera.aspect = aspect
      const fovRad = camera.fov * (Math.PI / 180)
      const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad)
      const rangeInv = 1.0 / (camera.near - camera.far)
      const matrix = [
        f / camera.aspect,
        0,
        0,
        0,
        0,
        f,
        0,
        0,
        0,
        0,
        (camera.near + camera.far) * rangeInv,
        -1,
        0,
        0,
        camera.near * camera.far * rangeInv * 2,
        0
      ]
      matrix[14] += camera.z
      matrix[15] += camera.z
      return matrix
    } else {
      return [
        2 / this.width,
        0,
        0,
        0,
        0,
        -2 / this.height,
        0,
        0,
        0,
        0,
        1,
        0,
        -1,
        1,
        0,
        1
      ]
    }
  }
  createShader(type, source) {
    const gl = this.gl
    const shader = gl.createShader(type)
    gl.shaderSource(shader, source)
    gl.compileShader(shader)
    if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      return shader
    } else {
      console.log(gl.getShaderInfoLog(shader))
      gl.deleteShader(shader)
    }
  }
  createProgram(vertex, fragment) {
    const gl = this.gl
    const vertexShader = this.createShader(gl.VERTEX_SHADER, vertex)
    const fragmentShader = this.createShader(gl.FRAGMENT_SHADER, fragment)
    const program = gl.createProgram()
    gl.attachShader(program, vertexShader)
    gl.attachShader(program, fragmentShader)
    gl.linkProgram(program)
    if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
      gl.useProgram(program)
      this.program = program
    } else {
      console.log(gl.getProgramInfoLog(program))
      gl.deleteProgram(program)
    }
  }
  createUniforms(data) {
    const gl = this.gl
    const uniforms = (this.data.uniforms = data)
    const values = (this.uniforms = {})
    Object.keys(uniforms).forEach(name => {
      const uniform = uniforms[name]
      uniform.location = gl.getUniformLocation(this.program, 'u_' + name)
      Object.defineProperty(values, name, {
        set: value => {
          uniforms[name].value = value
          this.setUniform(name, value)
        },
        get: () => uniforms[name].value
      })
    })
  }
  setUniform(name, value) {
    const gl = this.gl
    const uniform = this.data.uniforms[name]
    uniform.value = value
    switch (uniform.type) {
      case 'int': {
        gl.uniform1i(uniform.location, value)
        break
      }
      case 'float': {
        gl.uniform1f(uniform.location, value)
        break
      }
      case 'vec2': {
        gl.uniform2f(uniform.location, ...value)
        break
      }
      case 'vec3': {
        gl.uniform3f(uniform.location, ...value)
        break
      }
      case 'vec4': {
        gl.uniform4f(uniform.location, ...value)
        break
      }
      case 'mat2': {
        gl.uniformMatrix2fv(uniform.location, false, value)
        break
      }
      case 'mat3': {
        gl.uniformMatrix3fv(uniform.location, false, value)
        break
      }
      case 'mat4': {
        gl.uniformMatrix4fv(uniform.location, false, value)
        break
      }
    }
    // ivec2       : uniform2i,
    // ivec3       : uniform3i,
    // ivec4       : uniform4i,
    // sampler2D   : uniform1i,
    // samplerCube : uniform1i,
    // bool        : uniform1i,
    // bvec2       : uniform2i,
    // bvec3       : uniform3i,
    // bvec4       : uniform4i,
  }
  updateUniforms() {
    const gl = this.gl
    const uniforms = this.data.uniforms
    Object.keys(uniforms).forEach(name => {
      const uniform = uniforms[name]
      this.uniforms[name] = uniform.value
    })
  }
  createBuffers(data) {
    const gl = this.gl
    const buffers = (this.data.buffers = data)
    const values = (this.buffers = {})
    Object.keys(buffers).forEach(name => {
      const buffer = buffers[name]
      buffer.buffer = this.createBuffer('a_' + name, buffer.size)
      Object.defineProperty(values, name, {
        set: data => {
          buffers[name].data = data
          this.setBuffer(name, data)
          if (name == 'position') this.count = buffers.position.data.length / 3
        },
        get: () => buffers[name].data
      })
    })
  }
  createBuffer(name, size) {
    const gl = this.gl
    const program = this.program
    const index = gl.getAttribLocation(program, name)
    const buffer = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
    gl.enableVertexAttribArray(index)
    gl.vertexAttribPointer(index, size, gl.FLOAT, false, 0, 0)
    return buffer
  }
  setBuffer(name, data) {
    const gl = this.gl
    const buffers = this.data.buffers
    if (name == null && !gl.bindBuffer(gl.ARRAY_BUFFER, null)) return
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers[name].buffer)
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW)
  }
  updateBuffers() {
    const gl = this.gl
    const buffers = this.buffers
    Object.keys(buffers).forEach(name => (buffers[name] = buffer.data))
    this.setBuffer(null)
  }
  createTexture(src) {
    const gl = this.gl
    const texture = gl.createTexture()
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texImage2D(
      gl.TEXTURE_2D,
      0,
      gl.RGBA,
      1,
      1,
      0,
      gl.RGBA,
      gl.UNSIGNED_BYTE,
      new Uint8Array([0, 0, 0, 0])
    )
    this.texture = texture
    if (src) {
      this.uniforms.hasTexture = 1
      this.loadTexture(src)
    }
  }
  loadTexture(src) {
    const gl = this.gl
    const texture = this.texture
    const textureImage = new Image()
    textureImage.onload = () => {
      gl.bindTexture(gl.TEXTURE_2D, texture)
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        gl.RGBA,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        textureImage
      )
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
      // gl.generateMipmap( gl.TEXTURE_2D )
    }
    textureImage.src = src
  }
  update() {
    const gl = this.gl
    const now = performance.now()
    const elapsed = (now - this.time.start) / 5000
    const delta = now - this.time.old
    this.time.old = now
    this.uniforms.time = elapsed
    if (this.count > 0) {
      gl.clear(gl.COLORBUFFERBIT)
      gl.drawArrays(gl.POINTS, 0, this.count)
    }
    this.onUpdate(delta)
    requestAnimationFrame(this.update)
  }
}
const pointSize = 2.5
export default {
  name: 'BgAnimation',
  data() {
    return {}
  },
  mounted() {
    const pointSize = 2.5
    const waves = new ShaderProgram(document.querySelector('.waves'), {
      texture:
        '',
      uniforms: {
        size: { type: 'float', value: pointSize },
        field: { type: 'vec3', value: [0, 0, 0] },
        speed: { type: 'float', value: 5 }
      },
      vertex: `
    #define M_PI 3.1415926535897932384626433832795
    precision highp float;
    attribute vec4 a_position;
    attribute vec4 a_color;
    uniform float u_time;
    uniform float u_size;
    uniform float u_speed;
    uniform vec3 u_field;
    uniform mat4 u_projection;
    varying vec4 v_color;
    void main() {
      vec3 pos = a_position.xyz;
      pos.y += (
        cos(pos.x / u_field.x * M_PI * 8.0 + u_time * u_speed) +
        sin(pos.z / u_field.z * M_PI * 8.0 + u_time * u_speed)
      ) * u_field.y;
      gl_Position = u_projection * vec4( pos.xyz, a_position.w );
      gl_PointSize = ( u_size / gl_Position.w ) * 100.0;
      v_color = a_color;
    }`,
      fragment: `
    precision highp float;
    uniform sampler2D u_texture;
    varying vec4 v_color;
    void main() {
      gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);
    }`,
      onResize(w, h, dpi) {
        const position = []
        const color = []
        const width = 400 * (w / h)
        const depth = 400
        const height = 3
        const distance = 5
        for (let x = 0; x < width; x += distance) {
          for (let z = 0; z < depth; z += distance) {
            position.push(-width / 2 + x, -30, -depth / 2 + z)
            color.push(
              0,
              1 - (x / width) * 1,
              0.5 + (x / width) * 0.5,
              z / depth
            )
          }
        }
        this.uniforms.field = [width, height, depth]
        this.buffers.position = position
        this.buffers.color = color
        this.uniforms.size = (h / 400) * pointSize * dpi
      }
    })
  }
}
</script>
<style scoped>
canvas {
  display: block;
}
.waves {
  position: absolute;
  left: 2px;
  top: 0;
  right: 0;
  bottom: 0;
}
</style>
